Skip to content

let/const 与块级作用域面试题全解析

一、核心要点速览

💡 核心考点

  • 块级作用域: {} 内声明的变量只在该区域有效
  • 暂时性死区 (TDZ): 声明前访问会报 ReferenceError
  • const 本质: 保证绑定关系不变,而非值不变
  • 使用场景: 优先使用 const,需要重新赋值时用 let

二、var 的问题与块级作用域

1. var 的变量提升问题

javascript
// var 的问题:变量提升
console.log(a) // undefined(不会报错)
var a = 1

// 等价于:
var a
console.log(a) // undefined
a = 1

// 函数提升也会造成问题
if (false) {
  function fn() {
    console.log('never call')
  }
}
fn() // ❌ 仍然可以调用!

2. let/const 的块级作用域

javascript
// let/const: 块级作用域
{
  let b = 2
  const c = 3
  console.log(b) // 2
}
console.log(b) // ReferenceError: b is not defined
console.log(c) // ReferenceError: c is not defined

// for 循环中的应用
for (let i = 0; i < 5; i++) {
  console.log(i) // 0, 1, 2, 3, 4
}
console.log(i) // ReferenceError

// var 的情况
for (var j = 0; j < 5; j++) {
  console.log(j) // 0, 1, 2, 3, 4
}
console.log(j) // 4(泄漏到外部)

3. 经典面试题:setTimeout 与闭包

javascript
// ❌ var 版本
for (var i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i) // 5, 5, 5, 5, 5
  }, 1000)
}

// 原因:只有一个 i,所有定时器共享

// ✓ let 版本
for (let i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i) // 0, 1, 2, 3, 4
  }, 1000)
}

// 原因:每次循环都有新的 i
// 等价于:
// { let i = 0; setTimeout(...) }
// { let i = 1; setTimeout(...) }
// ...

// var 版本的解决方案(使用 IIFE)
for (var i = 0; i < 5; i++) {
  ((j) => {
    setTimeout(() => {
      console.log(j) // 0, 1, 2, 3, 4
    }, 1000)
  })(i)
}

三、暂时性死区 (TDZ)

1. TDZ 详解

┌──────────────────────────────────────────────────────────┐
│              暂时性死区 (Temporal Dead Zone)              │
└──────────────────────────────────────────────────────────┘

代码执行流程:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
function example() {
  console.log(x) // ❌ ReferenceError
  
  let x = 10
  
  console.log(y) // ❌ ReferenceError
  
  const y = 20
}

执行时序图:
时间 →  ─────────────────────────────────────────►

进入作用域


┌─────────────────┐
│ 创建 x, y 绑定   │ ← 已声明但未初始化
│ (存在于 TDZ)    │
└────────┬────────┘


    ┌─────────┐
    │ TDZ 区域 │ ← 访问会报 ReferenceError
    │ (禁止访问)│
    └─────────┘


    let x = 10


    ┌─────────┐
    │ x 可访问 │ ← 初始化完成
    └─────────┘


    const y = 20


    ┌─────────┐
    │ y 可访问 │
    └─────────┘

关键点:
✓ let/const 声明的变量在声明前不可访问
✓ TDZ 从作用域开始到变量声明处
✓ typeof 在 TDZ 内也会报错
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

2. TDZ 示例

javascript
// TDZ 示例 1
console.log(typeof x) // ReferenceError
let x = 10

// 如果在 TDZ 之前,typeof 应该返回 'undefined'
// 但在 TDZ 内,typeof 也会报错!

// TDZ 示例 2
function test() {
  console.log(a) // undefined(访问的是全局的 a)
  if (false) {
    let a = 1
  }
}
let a = 10
test()

// TDZ 示例 3:默认参数
function fn(x = y, y = 1) {
  console.log(x, y)
}
fn() // ReferenceError: y is not defined
// 因为 x 的默认值求值时,y 还在 TDZ 中

四、const 的本质

1. const 的特性

javascript
// const 保证的是绑定关系不变,而非值不变

// ✓ 基本类型:值不能变
const a = 1
a = 2 // TypeError: Assignment to constant variable.

// ✓ 引用类型:内存地址不能变,但属性可变
const obj = { name: 'Vue' }
obj.name = 'React' // ✓ 可以
obj.age = 3        // ✓ 可以
obj = {}           // ✗ TypeError

// ✓ 数组同理
const arr = [1, 2]
arr.push(3)      // ✓ 可以
arr[0] = 100     // ✓ 可以
arr.length = 0   // ✓ 可以
arr = []         // ✗ TypeError

// ✓ 对象冻结(完全不可变)
const frozen = Object.freeze({ name: 'Vue' })
frozen.name = 'React' // ✗ 严格模式下报错

2. 深度冻结对象

javascript
// 浅冻结
const obj = { name: 'Vue' }
Object.freeze(obj)
obj.name = 'React' // 无效

// 深冻结
function deepFreeze(obj) {
  Object.freeze(obj)
  
  Object.keys(obj).forEach(key => {
    const value = obj[key]
    if (typeof value === 'object' && value !== null) {
      deepFreeze(value)
    }
  })
}

const nested = {
  user: {
    name: 'Vue',
    skills: ['JS', 'CSS']
  }
}

deepFreeze(nested)
nested.user.name = 'React' // 无效
nested.user.skills.push('HTML') // 无效

五、实际应用场景

1. 使用场景决策树

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
场景选择指南:

需要重新赋值?

    ├─ 是 → 使用 let
    │   └─ 例如:循环计数器、累加器、状态标志

    └─ 否 → 使用 const
        └─ 90% 的情况应该用 const
            └─ 函数参数、配置对象、DOM 引用
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

2. 最佳实践示例

javascript
// ✓ 好的实践:优先使用 const
function processData(data) {
  const config = { timeout: 5000 }  // 配置不变
  let result = []                   // 需要累加
  
  for (let i = 0; i < data.length; i++) {  // 计数器
    const item = data[i]            // 循环内不变
    result.push(transform(item))
  }
  
  return result
}

// ✓ 好的实践:解构赋值用 const
const { name, age } = user
const [first, second] = array

// ✓ 好的实践:函数参数
function createUser({ name, email }) {
  // name 和 email 不应该被修改
  return { name, email }
}

// ✗ 避免:滥用 let
let config = { timeout: 5000 }
config = { timeout: 10000 }  // 为什么不直接用 const?

// ✓ 更好
const config = { timeout: 5000 }
// 如果需要新配置,创建新对象
const newConfig = { ...config, timeout: 10000 }

3. 循环中的选择

javascript
// ✓ for 循环:计数器用 let
for (let i = 0; i < arr.length; i++) {
  console.log(arr[i])
}

// ✓ for...of:推荐
for (const item of arr) {
  console.log(item)
}

// ✓ for...in:不推荐用于数组(会遍历原型链)
for (const key in obj) {
  if (obj.hasOwnProperty(key)) {
    console.log(key, obj[key])
  }
}

// ✗ while 循环:通常用 let
let count = 0
while (count < 10) {
  console.log(count++)
}

六、面试标准回答

let 和 const 是 ES6 引入的新声明方式,解决了 var 的多个问题。

主要区别

  1. 作用域:var 是函数作用域,let/const 是块级作用域({} 内有效)
  2. 变量提升:var 会提升到函数顶部,let/const 存在暂时性死区(TDZ),声明前无法访问
  3. 重复声明:var 允许,let/const 不允许
  4. 全局挂载:var 声明的全局变量会挂载到 window,let/const 不会

**暂时性死区(TDZ)**是指从进入作用域到变量声明处的区域,在这个区域内访问变量会报 ReferenceError。即使是 typeof 也会报错。

const 的本质是保证绑定关系不变,而非值不变。对于基本类型,值不能改变;对于引用类型,内存地址不能变,但属性可以修改。如果需要完全不可变,可以使用 Object.freeze() 深度冻结。

实际使用中,我遵循以下原则:

  • 优先使用 const(约占 90%)
  • 需要重新赋值时使用 let(如循环计数器、累加器)
  • 不再使用 var

经典应用是解决 setTimeout 循环问题:使用 let 声明循环变量,每次循环都会创建新的绑定,从而正确捕获当前值。


七、记忆口诀

let const 歌诀:

var 提升有问题,
let const 来解决。
块级作用域更安全,
暂时死区要牢记。

const 绑定不改变,
引用类型属性变。
优先 const 少用 let,
代码质量高一级!

八、推荐资源


九、总结一句话

  • let: 块级作用域 + 可重新赋值 = var 的现代替代
  • const: 常量绑定 + 引用不变 = 默认首选 🎯
  • TDZ: 声明前禁访问 = 避免提前使用错误 ⚠️
最近更新